// Copyright (C) 2011 Xamarin, Inc. All rights reserved.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using System.Reflection.PortableExecutable;
using System.Text;
using Microsoft.Build.Framework;

using Java.Interop.Tools.TypeNameMappings;
using Xamarin.Android.Tools;
using Microsoft.Android.Build.Tasks;

namespace Xamarin.Android.Tasks
{
	using PackageNamingPolicyEnum   = PackageNamingPolicy;

	/// <summary>
	/// Creates the native assembly containing the application config.
	/// </summary>
	public class GenerateNativeApplicationConfigSources : AndroidTask
	{
		public override string TaskPrefix => "GCA";

		[Required]
		public ITaskItem[] ResolvedAssemblies { get; set; } = [];

		public ITaskItem[]? NativeLibraries { get; set; }

		public ITaskItem[]? MonoComponents { get; set; }

		public ITaskItem[]? SatelliteAssemblies { get; set; }

		public bool UseAssemblyStore { get; set; }

		[Required]
		public string EnvironmentOutputDirectory { get; set; } = "";

		[Required]
		public string [] SupportedAbis { get; set; } = [];

		[Required]
		public string AndroidPackageName { get; set; } = "";

		[Required]
		public bool EnablePreloadAssembliesDefault { get; set; }

		[Required]
		public bool TargetsCLR { get; set; }

		public bool EnableMarshalMethods { get; set; }
		public bool EnableManagedMarshalMethodsLookup { get; set; }
		public string? RuntimeConfigBinFilePath { get; set; }
		public string ProjectRuntimeConfigFilePath { get; set; } = String.Empty;
		public string? BoundExceptionType { get; set; }

		public string? PackageNamingPolicy { get; set; }
		public string? Debug { get; set; }
		public ITaskItem[]? Environments { get; set; }
		public string? AndroidAotMode { get; set; }
		public bool AndroidAotEnableLazyLoad { get; set; }
		public bool EnableLLVM { get; set; }
		public string? HttpClientHandlerType { get; set; }
		public string? TlsProvider { get; set; }
		public string? AndroidSequencePointsMode { get; set; }
		public bool EnableSGenConcurrent { get; set; }
		public string? CustomBundleConfigFile { get; set; }

		bool _Debug {
			get {
				return string.Equals (Debug, "true", StringComparison.OrdinalIgnoreCase);
			}
		}

		static internal AndroidTargetArch GetAndroidTargetArchForAbi (string abi) => MonoAndroidHelper.AbiToTargetArch (abi);

		public override bool RunTask ()
		{
			bool usesMonoAOT = false;

			if (!Enum.TryParse (PackageNamingPolicy, out PackageNamingPolicy pnp)) {
				pnp = PackageNamingPolicyEnum.LowercaseCrc64;
			}

			AotMode aotMode = AotMode.None;
			if (!AndroidAotMode.IsNullOrEmpty () && Aot.GetAndroidAotMode (AndroidAotMode, out aotMode) && aotMode != AotMode.None) {
				usesMonoAOT = true;
			}

			SequencePointsMode sequencePointsMode;
			if (!Aot.TryGetSequencePointsMode (AndroidSequencePointsMode, out sequencePointsMode))
				sequencePointsMode = SequencePointsMode.None;

			// Even though environment files were potentially parsed in GenerateJavaStubs, we need to do it here again because we might have additional environment
			// files (generated by us) which weren't present by the time GeneratJavaStubs ran.
			var envBuilder = new EnvironmentBuilder (Log, EnablePreloadAssembliesDefault, sequencePointsMode);
			envBuilder.Read (Environments);

			if (_Debug) {
				envBuilder.AddDefaultDebugBuildLogLevel ();
			}
			envBuilder.AddDefaultMonoDebug ();
			envBuilder.AddHttpClientHandlerType (HttpClientHandlerType);
			envBuilder.AddMonoGcParams (EnableSGenConcurrent);

			global::Android.Runtime.BoundExceptionType boundExceptionType;
			if (String.IsNullOrEmpty (BoundExceptionType) || MonoAndroidHelper.StringEquals (BoundExceptionType, "System", StringComparison.OrdinalIgnoreCase)) {
				boundExceptionType = global::Android.Runtime.BoundExceptionType.System;
			} else if (MonoAndroidHelper.StringEquals (BoundExceptionType, "Java", StringComparison.OrdinalIgnoreCase)) {
				boundExceptionType = global::Android.Runtime.BoundExceptionType.Java;
			} else {
				throw new InvalidOperationException ($"Unsupported BoundExceptionType value '{BoundExceptionType}'");
			}

			int assemblyNameWidth = 0;
			Encoding assemblyNameEncoding = Encoding.UTF8;

			Action<ITaskItem> updateNameWidth = (ITaskItem assembly) => {
				if (UseAssemblyStore) {
					return;
				}

				string assemblyName = Path.GetFileName (assembly.ItemSpec);
				int nameBytes = assemblyNameEncoding.GetBytes (assemblyName).Length;
				if (nameBytes > assemblyNameWidth) {
					assemblyNameWidth = nameBytes;
				}
			};

			int assemblyCount = 0;
			HashSet<string>? archAssemblyNames = null;
			HashSet<string> uniqueAssemblyNames = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
			Action<ITaskItem> updateAssemblyCount = (ITaskItem assembly) => {
				string? culture = MonoAndroidHelper.GetAssemblyCulture (assembly);
				string fileName = Path.GetFileName (assembly.ItemSpec);
				string assemblyName;

				if (String.IsNullOrEmpty (culture)) {
					assemblyName = fileName;
				} else {
					assemblyName = $"{culture}/{fileName}";
				}

				if (!uniqueAssemblyNames.Contains (assemblyName)) {
					uniqueAssemblyNames.Add (assemblyName);
				}

				archAssemblyNames ??= new HashSet<string> (StringComparer.OrdinalIgnoreCase);

				if (!archAssemblyNames.Contains (assemblyName)) {
					assemblyCount++;
					archAssemblyNames.Add (assemblyName);
				}
			};

			if (SatelliteAssemblies != null) {
				foreach (ITaskItem assembly in SatelliteAssemblies) {
					updateNameWidth (assembly);
					updateAssemblyCount (assembly);
				}
			}

			int android_runtime_jnienv_class_token = -1;
			int jnienv_initialize_method_token = -1;
			int jnienv_registerjninatives_method_token = -1;
			foreach (var assembly in ResolvedAssemblies) {
				updateNameWidth (assembly);
				updateAssemblyCount (assembly);

				if (android_runtime_jnienv_class_token != -1) {
					continue;
				}

				if (!assembly.ItemSpec.EndsWith ("Mono.Android.dll", StringComparison.OrdinalIgnoreCase)) {
					continue;
				}

				GetRequiredTokens (assembly.ItemSpec, out android_runtime_jnienv_class_token, out jnienv_initialize_method_token, out jnienv_registerjninatives_method_token);
			}

			if (!UseAssemblyStore) {
				int abiNameLength = 0;
				foreach (string abi in SupportedAbis) {
					if (abi.Length <= abiNameLength) {
						continue;
					}
					abiNameLength = abi.Length;
				}
				assemblyNameWidth += abiNameLength + 2; // room for '/' and the terminating NUL
			}

			MonoComponent monoComponents = MonoComponent.None;
			if (MonoComponents != null && MonoComponents.Length > 0) {
				foreach (ITaskItem item in MonoComponents) {
					if (MonoAndroidHelper.StringEquals ("diagnostics_tracing", item.ItemSpec, StringComparison.OrdinalIgnoreCase)) {
						monoComponents |= MonoComponent.Tracing;
					} else if (MonoAndroidHelper.StringEquals ("hot_reload", item.ItemSpec, StringComparison.OrdinalIgnoreCase)) {
						monoComponents |= MonoComponent.HotReload;
					} else if (MonoAndroidHelper.StringEquals ("debugger", item.ItemSpec, StringComparison.OrdinalIgnoreCase)) {
						monoComponents |= MonoComponent.Debugger;
					}
				}
			}

			var uniqueNativeLibraries = new List<ITaskItem> ();
			var seenNativeLibraryNames = new HashSet<string> (StringComparer.OrdinalIgnoreCase);
			if (NativeLibraries != null) {
				foreach (ITaskItem item in NativeLibraries) {
					// We don't care about different ABIs here, just the file name
					string name = Path.GetFileName (item.ItemSpec);
					if (seenNativeLibraryNames.Contains (name)) {
						continue;
					}

					seenNativeLibraryNames.Add (name);
					uniqueNativeLibraries.Add (item);
				}
			}

			bool haveRuntimeConfigBlob = !String.IsNullOrEmpty (RuntimeConfigBinFilePath) && File.Exists (RuntimeConfigBinFilePath);
			var jniRemappingNativeCodeInfo = BuildEngine4.GetRegisteredTaskObjectAssemblyLocal<GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfo> (ProjectSpecificTaskObjectKey (GenerateJniRemappingNativeCode.JniRemappingNativeCodeInfoKey), RegisteredTaskObjectLifetime.Build);
			LLVMIR.LlvmIrComposer appConfigAsmGen;

			if (TargetsCLR) {
				Dictionary<string, string>? runtimeProperties = RuntimePropertiesParser.ParseConfig (ProjectRuntimeConfigFilePath);
				appConfigAsmGen = new ApplicationConfigNativeAssemblyGeneratorCLR (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, runtimeProperties, Log) {
					UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload,
					AndroidPackageName = AndroidPackageName,
					PackageNamingPolicy = pnp,
					JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent,
					NumberOfAssembliesInApk = assemblyCount,
					BundledAssemblyNameWidth = assemblyNameWidth,
					NativeLibraries = uniqueNativeLibraries,
					AndroidRuntimeJNIEnvToken = android_runtime_jnienv_class_token,
					JNIEnvInitializeToken = jnienv_initialize_method_token,
					JNIEnvRegisterJniNativesToken = jnienv_registerjninatives_method_token,
					JniRemappingReplacementTypeCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementTypeCount,
					JniRemappingReplacementMethodIndexEntryCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementMethodIndexEntryCount,
					MarshalMethodsEnabled = EnableMarshalMethods,
					ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup,
					IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
				};
			} else {
				appConfigAsmGen = new ApplicationConfigNativeAssemblyGenerator (envBuilder.EnvironmentVariables, envBuilder.SystemProperties, Log) {
					UsesMonoAOT = usesMonoAOT,
					UsesMonoLLVM = EnableLLVM,
					UsesAssemblyPreload = envBuilder.Parser.UsesAssemblyPreload,
					MonoAOTMode = aotMode.ToString ().ToLowerInvariant (),
					AotEnableLazyLoad = AndroidAotEnableLazyLoad,
					AndroidPackageName = AndroidPackageName,
					BrokenExceptionTransitions = envBuilder.Parser.BrokenExceptionTransitions,
					PackageNamingPolicy = pnp,
					BoundExceptionType = boundExceptionType,
					JniAddNativeMethodRegistrationAttributePresent = NativeCodeGenState.TemplateJniAddNativeMethodRegistrationAttributePresent,
					HaveRuntimeConfigBlob = haveRuntimeConfigBlob,
					NumberOfAssembliesInApk = assemblyCount,
					BundledAssemblyNameWidth = assemblyNameWidth,
					MonoComponents = (MonoComponent)monoComponents,
					NativeLibraries = uniqueNativeLibraries,
					HaveAssemblyStore = UseAssemblyStore,
					AndroidRuntimeJNIEnvToken = android_runtime_jnienv_class_token,
					JNIEnvInitializeToken = jnienv_initialize_method_token,
					JNIEnvRegisterJniNativesToken = jnienv_registerjninatives_method_token,
					JniRemappingReplacementTypeCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementTypeCount,
					JniRemappingReplacementMethodIndexEntryCount = jniRemappingNativeCodeInfo == null ? 0 : jniRemappingNativeCodeInfo.ReplacementMethodIndexEntryCount,
					MarshalMethodsEnabled = EnableMarshalMethods,
					ManagedMarshalMethodsLookupEnabled = EnableManagedMarshalMethodsLookup,
					IgnoreSplitConfigs = ShouldIgnoreSplitConfigs (),
				};
			}
			LLVMIR.LlvmIrModule appConfigModule = appConfigAsmGen.Construct ();

			foreach (string abi in SupportedAbis) {
				string targetAbi = abi.ToLowerInvariant ();
				string environmentBaseAsmFilePath = Path.Combine (EnvironmentOutputDirectory, $"environment.{targetAbi}");
				string environmentLlFilePath  = $"{environmentBaseAsmFilePath}.ll";
				AndroidTargetArch targetArch = GetAndroidTargetArchForAbi (abi);

				using var appConfigWriter = MemoryStreamPool.Shared.CreateStreamWriter ();
				try {
					appConfigAsmGen.Generate (appConfigModule, targetArch, appConfigWriter, environmentLlFilePath);
				} catch {
					throw;
				} finally {
					appConfigWriter.Flush ();
					Files.CopyIfStreamChanged (appConfigWriter.BaseStream, environmentLlFilePath);
				}
			}

			return !Log.HasLoggedErrors;
		}

		bool ShouldIgnoreSplitConfigs ()
		{
			if (CustomBundleConfigFile.IsNullOrEmpty ()) {
				return false;
			}

			return BundleConfigSplitConfigsChecker.ShouldIgnoreSplitConfigs (Log, CustomBundleConfigFile);
		}

		void GetRequiredTokens (string assemblyFilePath, out int android_runtime_jnienv_class_token, out int jnienv_initialize_method_token, out int jnienv_registerjninatives_method_token)
		{
			using (var pe = new PEReader (File.OpenRead (assemblyFilePath))) {
				GetRequiredTokens (pe.GetMetadataReader (), out android_runtime_jnienv_class_token, out jnienv_initialize_method_token, out jnienv_registerjninatives_method_token);
			}

			if (android_runtime_jnienv_class_token == -1 || jnienv_initialize_method_token == -1 || jnienv_registerjninatives_method_token == -1) {
				throw new InvalidOperationException ($"Unable to find the required Android.Runtime.JNIEnvInit method tokens for {assemblyFilePath}");
			}
		}

		void GetRequiredTokens (MetadataReader reader, out int android_runtime_jnienv_class_token, out int jnienv_initialize_method_token, out int jnienv_registerjninatives_method_token)
		{
			android_runtime_jnienv_class_token = -1;
			jnienv_initialize_method_token = -1;
			jnienv_registerjninatives_method_token = -1;

			TypeDefinition? typeDefinition = null;

			foreach (TypeDefinitionHandle typeHandle in reader.TypeDefinitions) {
				TypeDefinition td = reader.GetTypeDefinition (typeHandle);
				if (!TypeMatches (td)) {
					continue;
				}

				typeDefinition = td;
				android_runtime_jnienv_class_token = MetadataTokens.GetToken (reader, typeHandle);
				break;
			}

			if (typeDefinition == null) {
				return;
			}

			foreach (MethodDefinitionHandle methodHandle in typeDefinition.Value.GetMethods ()) {
				MethodDefinition md = reader.GetMethodDefinition (methodHandle);
				string name = reader.GetString (md.Name);

				if (jnienv_initialize_method_token == -1 && MonoAndroidHelper.StringEquals (name, "Initialize")) {
					jnienv_initialize_method_token = MetadataTokens.GetToken (reader, methodHandle);
				} else if (jnienv_registerjninatives_method_token == -1 && MonoAndroidHelper.StringEquals (name, "RegisterJniNatives")) {
					jnienv_registerjninatives_method_token = MetadataTokens.GetToken (reader, methodHandle);
				}

				if (jnienv_initialize_method_token != -1 && jnienv_registerjninatives_method_token != -1) {
					break;
				}
			}


			bool TypeMatches (TypeDefinition td)
			{
				string ns = reader.GetString (td.Namespace);
				if (!MonoAndroidHelper.StringEquals (ns, "Android.Runtime")) {
					return false;
				}

				string name = reader.GetString (td.Name);
				if (!MonoAndroidHelper.StringEquals (name, "JNIEnvInit")) {
					return false;
				}

				return true;
			}
		}
	}
}
